<?php

/**
 * Contains database functionality using the mysqli driver.
 *
 * PHP Version 5
 *
 * @category BuyLocal
 * @package  BuyLocal
 * @author   Kevin Fodness <kevin.fodness@gmail.com>
 * @author   Mike Pennisi <mpennisi@andrew.cmu.edu>
 * @author   Aaron Ryden <aryden@gmail.com>
 * @author   Spencer Hakim <hakims@rpi.edu>
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
 * @link     http://code.google.com/p/buylocalopensourcerensselaer/
 */
 
/** @uses escapeRaw To display HTML while preserving newlines. */
require_once 'function_escaperaw.php';

/** @uses makeHash To make a standardized hash of a string. */
require_once 'function_makehash.php';

/** @uses valueOf For safe handling of arrays. */
require_once 'function_valueof.php';

/** @uses validEmail For validating emails. */
require_once 'function_validemail.php';
 
/**
 * A class to handle all database functionality using the mysqli driver.
 *
 * @category BuyLocal
 * @package  BuyLocal
 * @author   Kevin Fodness <kevin.fodness@gmail.com>
 * @author   Mike Pennisi <mpennisi@andrew.cmu.edu>
 * @author   Aaron Ryden <aryden@gmail.com>
 * @author   Spencer Hakim <hakims@rpi.edu>
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
 * @link     http://code.google.com/p/buylocalopensourcerensselaer/
 */ 
class Database
{
    /**
     * The mysqli object variable used in this class.
     *
     * @access private
     * @var object
     */
    private $_mysqli;
    
    /**
     * The constructor function.  Establishes a connection to the database. 
     *
     * @param string $dbServer The server to connect to.
     * @param string $dbUser   The username to use to connect to the server.
     * @param string $dbPass   The password to use to connect to the server.
     * @param string $dbName   The name of the database to connect to.
     *
     * @access public
     * @return null
     */
    public function __construct($dbServer, $dbUser, $dbPass, $dbName)
    {
        $this->_mysqli = new mysqli($dbServer, $dbUser, $dbPass, $dbName)
            or trigger_error($this->_mysqli->error, E_USER_ERROR);
    }
    
    /**
     * Destructor function.  Closes the connection to the database. 
     *
     * @access public
     * @return null
     */
    public function __destruct()
    {
        if (isset($this->_mysqli)) {
            $this->_mysqli->close();
        }
    }
    
    /**
     * Function to clear results from a multi query.
     *
     * If this is not done after a non-select operation (INSERT, etc) then
     * the database pointer will not be freed, and future queries will be
     * blocked by the server.
     *
     * @access private
     * @return null
     */
    private function _clearResults()
    {
        do {
            $result = $this->_mysqli->store_result();
            if ($result) {
                $result->close();
            }
        } while ($this->_mysqli->next_result());
    }
    
    /**
     * Function to run real_escape_string recursively on an input.
     *
     * @param mixed &$source The object, array, or variable to escape.
     *
     * @access private
     * @return null
     */
    private function _escape(&$source)
    {
        if (is_array($source) || is_object($source)) { 
            foreach ($source as &$var) {
                $this->_escape($var);
            }
        } else {
            $source = $this->_mysqli->real_escape_string($source);
        }
    }
    
    /**
     * Function to flush the contents of a table.
     *
     * @param string $tableName The name of the table to truncate.
     *
     * @access private
     * @return null
     */
    private function _flush($tableName)
    {
        $this->_escape($tableName);
        $sql = 'TRUNCATE TABLE `' . $tableName . '`;';
        $this->_execute($sql);
    }
    
    /**
     * Function to execute a query and return a result set.
     *
     * @param string $sql The SQL code to execute.
     *
     * @access private
     * @return object  A mysqli result object.
     */
    private function _getResult($sql)
    {
        $this->_mysqli->real_query($sql)
            or trigger_error(
                $this->_mysqli->error .
                "\nmysqli->realquery()\n" . $sql,
                E_USER_ERROR
            );
        $result = $this->_mysqli->store_result()
            or trigger_error(
                $this->_mysqli->error .
                "\nmysqli->store_result()\n" . $sql,
                E_USER_ERROR
            );
        return $result;
    }
    
    /**
     * Function to return a single object from a SQL query.
     *
     * @param string $sql The SQL code to execute.
     *
     * @access private
     * @return object  The result from $result->fetch_object()
     */
    private function _getObject($sql)
    {
        $result = $this->_getResult($sql);
        return $result->fetch_object();
    }
    
    /**
     * Function to execute a query that does not produce a result (insert etc)
     *
     * @param string $sql The SQL code to execute.
     *
     * @access private
     * @return null
     */
    private function _execute($sql)
    {
        $this->_mysqli->multi_query($sql)
            or trigger_error($this->_mysqli->error, E_USER_ERROR);
        while ($this->_mysqli->more_results()) {
            if (!$this->_mysqli->next_result()) {
                trigger_error($this->_mysqli->error, E_USER_ERROR);
            }
        }
        $this->_clearResults();
    }

    /**
     * Function to execute a multi-query within a transaction.
     *
     * @param string $sql The SQL code to execute.
     *
     * @access private
     * @return bool True on success
     */
    private function _executeTransaction($sql)
    {
        /**
         * Make entire query a transaction.
         *
         * COMMIT will never be reached if there is an error that stops the query
         * altogether.
         */
        $sql = 'START TRANSACTION;' . "\n" . $sql . "\n" . 'COMMIT;';
        
        /** Error check each statement in transaction. */
        $this->_mysqli->multi_query($sql)
            or trigger_error(
                $this->_mysqli->error . "\nmysqli->multi_query()\n" . $sql,
                E_USER_ERROR
            );
        while ($this->_mysqli->more_results()) {
            if (!$this->_mysqli->next_result()) {
                trigger_error(
                    $this->_mysqli->error . "\nmysqli->next_result()\n" . $sql,
                    E_USER_ERROR
                );
            }
        }
        $this->_clearResults();

        /**
         * Rollback changes on failed transaction.
         *
         * If COMMIT was hit, this does nothing. Otherwise, it puts things back how
         * they were (silently, albeit).
         */
        $this->_mysqli->rollback();
        return true;
    }
    
    /**
     * A function to load the contents of a text file into the database raw.
     *
     * @param string $filename The filename to load.
     *
     * @access public
     * @return null
     */
    public function loadFile($filename)
    {
        $this->_execute(file_get_contents($filename, FILE_USE_INCLUDE_PATH));
    }
    
    /**
     * A function to load the contents of a text file into the database.
     *
     * @param string $filename The filename to load.
     *
     * @access public
     * @return null
     */
    public function loadFileEach($filename)
    {
        $raw = split(';', file_get_contents($filename, FILE_USE_INCLUDE_PATH));
        foreach ($raw as $sql) {
            $sql = trim($sql);
            if (strlen($sql) > 0) {
                $this->_execute($sql);
            }
        }
    }
    
    /**
     * A function to load a raw SQL query.  Use ONLY for database upgrades!
     *
     * @param string $sql The SQL to execute.
     *
     * @access public
     * @return null
     */
    public function loadQuery($sql)
    {
        $this->_execute($sql);
    }
    
    /**
     * Function to add a user to the database.
     *
     * @param object $user     A User object to insert into the database.
     * @param string $password The user's password.
     *
     * @access public
     * @return mixed  True on success, or an error message on failure.
     */
    public function addUser($user, $password)
    {
        /** Objects act as if passed by reference, so clone them for safety. */
        if ($user != null) {
            $user = clone $user;
        }

        /** Determines if all of the required information was provided. */
        if ($user->email == '') {
            return 'A valid email address must be provided.';
        }
        if ($user->zip->zip == '') {
            return 'A valid ZIP code must be provided.';
        }
        if ($password == '') {
            return 'A password must be provided.';
        }
        
        /** Escapes the input. */
        $this->_escape($user);
        $this->_escape($password);
        
        /** Determines if the email address already exists in the database. */
        if ($this->getUserByEmail($user->email) !== false) {
            return 'The email address you provided is already in use.';
        }
        
        /** Verifies that none of the maximum field lengths are exceeded. */
        if (strlen($user->email) > L_EMAIL) {
            return 'The email address cannot exceed ' . L_EMAIL
                . ' characters.';
        }
        if (strlen($password) > L_PASSWORD) {
            return 'The password cannot exceed ' . L_PASSWORD . ' characters.';
        }
        if (strlen($user->zip->zip) > L_ZIP || strlen($user->zip->zip) < 3) {
            return 'The zip code is not valid.';
        }
        if (strlen($user->name) > L_NAME) {
            return 'The name cannot exceed ' . L_NAME . ' characters.';
        }
        foreach ($user->phoneNumbers as $phone) {
            if (strlen($phone->number) > L_PHONE) {
                return 'The phone number cannot exceed ' . L_PHONE 
                    . 'characters.';
            }
            if (strlen($phone->description) > L_CONTACTDESC) {
                return 'The phone description cannot exceed ' . L_CONTACTDESC
                    . ' characters.';
            }
        }
        
        /** Adds the user. */
        $sql = 'INSERT INTO `users` (`roleid`, `password`, `email`, `zip`, '
            . '`isactive`, `registeredon`) VALUES '
            . '(\'' . $user->role . '\', '
            . '\'' . makeHash($password) . '\', '
            . '\'' . $user->email . '\', '
            . '\'' . $user->zip->zip . '\', '
            . '0, NOW());';
        $this->_execute($sql);
        
        /** Gets the user object for the user that was just created. */
        $newUser = $this->getUserByEmail($user->email);
        
        /** Adds the user's name and phone number(s) if necessary. */
        if ($user->name != '') {
            $sql = 'INSERT INTO `usernames` (`userid`, `name`) VALUES '
                . '(\'' . $newUser->id . '\', \'' . $user->name . '\');';
            $this->_execute($sql);
        }
        foreach ($user->phoneNumbers as $phone) {
            $sql = 'INSERT INTO `userphones` '
                . '(`userid`, `phone`, `description`) VALUES '
                . '(\'' . $newUser->id . '\', '
                . '\'' . $phone->number . '\', '
                . '\'' . $phone->description . '\');';
            $this->_execute($sql);
        }

        /** Adds the confirmation code and sends the activation email. */
        $activationReturn = $this->addActivationCode($newUser);
        if ($activationReturn === true) {
            return true;
        } else if ($activationReturn === false) {
            return 'Failed to create or send activation code.';
        } else { /** Used for test server without a Sendmail system */
            return 'Failed to send activation code via email. Please click '
                . '<a href="' .$activationReturn
                . '">here</a> to activate your account.';
        }
    }
    
    /**
     * Function to add an activation code and send an activation email.
     *
     * @param object $user The user to add an activation code for.
     *
     * @access public
     * @return mixed  Returns true on success, or an error message on failure.
     */
    public function addActivationCode($user)
    {
        /** Objects act as if passed by reference, so clone them for safety. */
        if ($user != null) {
            $user = clone $user;
        }
        $this->_escape($user);
        
        if ($user->id == '') {
            return 'Invalid user ID.';
        }
        if ($user->email == '') {
            return 'No email address to send the activation code to.';
        }
        
        /** Adds activation code to the database. */
        $code = substr(makeHash(rand()), 0, 10);
        $sql  = 'INSERT INTO `useractivation` (`userid`, `code`) VALUES '
            . '(\'' . $user->id . '\', \'' . $code . '\');';
        $this->_execute($sql);
        
        /** Sends activation email to the user. */
        $subject       = SITENAME . ' Account Activation';
        $activationUrl = BASE_URL . '/register.php?code=' . $code
            . '&email=' . urlencode($user->email);
        $message       = '<html><head><title>' . $subject . '</title></head><body>'
            . '<h1>Welcome to ' . SITENAME . '!</h1>'
            . '<p>In order to activate your account, either click the link '
            . 'below or paste it into the location bar in your web browser.</p>'
            . '<p><a href="' . $activationUrl . '">' . $activationUrl
            . '</a></p></body></html>';
        $html          = true;
        $mail          = new Sendmail($user->email, $subject, $message, $html);
        
        if ($mail->send()) {
            return true;
        }
        
        return $activationUrl;
    }
    
    /**
     * Function to activate a user account.
     *
     * @param string $email The email address to look up.
     * @param string $code  The activation code to look up.
     *
     * @access public
     * @return bool   True on success, false on failure.
     */
    public function activateUser($email, $code)
    {
        if ($email == '' || $code == '') {
            return false;
        }
        $this->_escape($email);
        $this->_escape($code);

        /** Checks to see if a user exists for the given email. */
        $user = $this->getUserByEmail($email);
        if (!$user) {
            return 'No user exists for this email address.';
        }
        if ($user->id == '') {
            return 'No user exists for this email address.';
        }

        /** Checks to see if the activation code exists for the given user. */
        $sql = 'SELECT `userid` '
            . 'FROM `useractivation` '
            . 'WHERE `userid` = \'' . $user->id . '\' '
            . 'AND `code` = \'' . $code . '\';';
        $result = $this->_getResult($sql);
        if ($result->num_rows == 0) {
            return 'Incorrect activation code.';
        }

        /** Activates the user account. */
        $sql = 'UPDATE `users` '
            . 'SET `isactive` = 1 '
            . 'WHERE `userid` = \'' . $user->id . '\';';
        $this->_execute($sql);
        
        /** Removes the activation code from the activation table. */
        $sql = 'DELETE FROM `useractivation` '
            . 'WHERE `userid` = \'' . $user->id . '\';';
        $this->_execute($sql);
        return true;
    }
    
    /**
     * Function to process a user login.
     *
     * @param string $username The username to check.
     * @param string $password The password to check.
     *
     * @access public
     * @return string sessionId on successful login, false on failure.
     */
    public function login($username, $password)
    {
        /** Initial check to ensure the username and password aren't blank. */
        if ($username == '' || $password == '') {
            return false;
        }
        
        /** Escapes the input. */
        $this->_escape($username);
        $this->_escape($password);
        
        /** Checks to see whether the userid exists by getting the salt. */
        $sql    = 'SELECT `salt` '
            . 'FROM `users` '
            . 'WHERE `email` = \'' . $username . '\' '
            . 'AND `isactive` = 1;';
        $result = $this->_getResult($sql);
        if ($result->num_rows == 0) {
            $result->close();
            return false;
        }
        $obj = $result->fetch_object();
        $result->close();
        $salt = $obj->salt;
        
        /** Processes the password hash based on the value of the salt. */
        if ($salt == '') {
            $passwordHash = makeHash($password);
        } else {
            $passwordHash = sha1($salt . $password);
        }
        
        /** Checks the username and password combination. */
        $sql    = 'SELECT `userid` '
            . 'FROM `users` '
            . 'WHERE `email` = \'' . $username . '\' '
            . 'AND `password` = \'' . $passwordHash . '\';';
        $result = $this->_getResult($sql);
        if ($result->num_rows == 0) {
            $result->close();
            return false;
        }
        $obj = $result->fetch_object();
        $result->close();
        $userId = $obj->userid;
        
        /** Performs housekeeping on the sessions table by removing old entries. */
        $sessionTimeout = EPOCH - SESSIONLENGTH;
        $sql            = 'DELETE FROM `sessions` '
            . 'WHERE `lastactive` < FROM_UNIXTIME(' . $sessionTimeout . ') '
            . 'OR `userid` = \'' . $userId . '\';';
        $this->_execute($sql);
        
        /** Processes the login by adding an entry to the session table. */
        do {
            $sessionId = makeHash(rand());
        } while ($this->sessionIdExists($sessionId));
        $sql = 'INSERT INTO `sessions` '
            . '(`sessionid`, `userid`, `ip`, `lastactive`) VALUES '
            . '(\'' . $sessionId . '\', '
            . '\'' . $userId . '\', '
            . '\'' . $_SERVER['REMOTE_ADDR'] . '\', '
            . 'NOW());';
        $this->_execute($sql);
        
        /** Updates the last logon field in the users table. */
        $sql = 'UPDATE `users` '
            . 'SET `lastlogon` = NOW() '
            . 'WHERE `email` = \'' . $username . '\';';
        $this->_execute($sql);
        
        /** Updates the database with the new password hash if necessary. */
        if ($salt == '') {
            return $sessionId;
        }
        $passwordHash = makeHash($password);
        $sql          = 'UPDATE `users` '
            . 'SET `salt` = NULL, `password` = \'' . $passwordHash . '\' '
            . 'WHERE `email` = \'' . $username . '\';';
        $this->_execute($sql);
        return $sessionId;
    }
    
    /**
     * Function to update when a user's session was last active.
     *
     * @param int $userId User ID number
     *
     * @access public
     * @return null
     */
    public function updateSessionTimeout($userId)
    {
        $this->_escape($userId);
        $sessionTimeout = EPOCH - SESSIONLENGTH;
        $sql            = 'UPDATE `sessions` '
            . 'SET `lastactive` = NOW() '
            . 'WHERE `sessionid` = \'' .valueOf($_SESSION, 'sessionid'). '\' '
            . 'AND `userid` = \'' . $userId . '\';';
        $this->_execute($sql);
    }
    
    /**
     * Function to determine if a session ID exists in the database or not.
     *
     * @param string $sessionId The session ID to check in the database.
     *
     * @access public
     * @return bool True on successful find, false on failure.
     */
    public function sessionIdExists($sessionId)
    {
        $this->_escape($sessionId);
        $sql    = 'SELECT `sessionid` '
            . 'FROM `sessions` '
            . 'WHERE `sessionid` = \'' . $sessionId . '\';';
        $result = $this->_getResult($sql);
        return ($result->num_rows == 0) ? false : true;
    }
    
    /**
     * Function to get a User object containing information on the current user.
     *
     * @access public
     * @return object
     */
    public function getCurrentUser()
    {
        /** Gets the session ID. */
        $sessionId = valueOf($_SESSION, 'sessionid');
        $this->_escape($sessionId);
        
        /** Gets the session ZIP. */
        if (valueOf($_SESSION, 'zip') != '') {
            $zip = $this->getZipByZip($_SESSION['zip']);
        } else {
            $zip = $this->getZipByZip(D_ZIP);
        }
        
        $guest = new User(0, 'Guest', '', $zip, 0, true);
        
        /** If the session ID is blank, return a guest user. */
        if ($sessionId == '') {
            return $guest;
        }
        
        /** Attempt to find a valid session in the database. */
        $sessionTimeout = EPOCH - SESSIONLENGTH;
        $sql            = 'SELECT `userid` '
            . 'FROM `sessions` '
            . 'WHERE `sessionid` = \'' . $sessionId . '\' '
            . 'AND `ip` = \'' . $_SERVER['REMOTE_ADDR'] . '\' '
            . 'AND `lastactive` > FROM_UNIXTIME(' . $sessionTimeout . ');';
        $result         = $this->_getResult($sql);
        if ($result->num_rows == 0) {
            $_SESSION['sessionid'] = '';
            $this->deleteSessionById($sessionId);
            return $guest;
        }
        
        /** The user is currently logged in. Gets user info. */
        $obj    = $result->fetch_object();
        $userId = $obj->userid;

        /** The user is currently logged in, update session lastactive time. */
        $this->updateSessionTimeout($userId);

        /** Gets the user information other than the phone number. */
        $sql    = 'SELECT `users`.`userid`, `usernames`.`name`, `users`.`email`, '
            . '`users`.`zip`, `users`.`roleid`, `users`.`isactive`, ' 
            . '`users`.`registeredon`, `users`.`lastlogon` '
            . 'FROM `users` '
            . 'LEFT OUTER JOIN `usernames` '
            . 'ON `users`.`userid` = `usernames`.`userid` '
            . 'WHERE `users`.`userid` = \'' . $userId . '\';';
        $result = $this->_getResult($sql);
        if ($result->num_rows == 0) {
            return $guest;
        }
        $obj = $result->fetch_object();
        if (valueOf($_SESSION, 'zip') == '') {
            $zip = $this->getZipByZip($obj->zip);
        }
        $user = new User($obj->userid, $obj->name, $obj->email, $zip, 
            $obj->roleid, $obj->isactive, $obj->registeredon, $obj->lastlogon,
            array()
        );

        /** Gets phone numbers for this user. */
        $sql    = 'SELECT `phone`, `description` '
            . 'FROM `userphones` '
            . 'WHERE `userid` = \'' . $userId . '\';';
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $user->phoneNumbers[] = new Phone($obj->phone, $obj->description);
        }
        
        /** Returns the user object. */
        return $user;
    }
    
    /**
     * Function to delete a row from the sessions table based on a session ID.
     *
     * @param string $sessionId The session ID to delete.
     *
     * @access public
     * @return null
     */
    public function deleteSessionById($sessionId)
    {
        $this->_escape($sessionId);
        $sql = 'DELETE FROM `sessions` '
            . 'WHERE `sessionid` = \'' . $sessionId . '\';';
        $this->_execute($sql);
    }
    
    /**
     * Function to get information on a user based on a user ID.
     *
     * @param int $userId The user ID to look up.
     *
     * @access public
     * @return object A User object.
     */
    public function getUserById($userId)
    {
        $this->_escape($userId);
        
        /** Gets the user information other than the phone number. */
        $sql    = 'SELECT `users`.`userid`, `usernames`.`name`, `users`.`email`, '
            . '`users`.`zip`, `users`.`roleid`, `users`.`isactive`, ' 
            . '`users`.`registeredon`, `users`.`lastlogon` '
            . 'FROM `users` '
            . 'LEFT OUTER JOIN `usernames` '
            . 'ON `users`.`userid` = `usernames`.`userid` '
            . 'WHERE `users`.`userid` = \'' . $userId . '\';';
        $result = $this->_getResult($sql);
        if ($result->num_rows == 0) {
            return false;
        }
        $obj  = $result->fetch_object();
        $zip  = $this->getZipByZip($obj->zip);
        if ($zip === false) {
            $zip = $this->getZipByZip(D_ZIP);
        }
        $user = new User($obj->userid, $obj->name, $obj->email, $zip, 
            $obj->roleid, $obj->isactive, $obj->registeredon, $obj->lastlogon,
            array()
        );

        /** Gets phone numbers for this user. */
        $sql    = 'SELECT `phone`, `description` '
            . 'FROM `userphones` '
            . 'WHERE `userid` = \'' . $userId . '\';';
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $user->phoneNumbers[] = new Phone($obj->phone, $obj->description);
        }
        
        /** Returns the user object. */
        return $user;
    }

    /**
     * Function to build array of Category names/IDs from database.
     *
     * @param bool $getAll Specifies whether to get all categories, or only main ones
     *
     * @access public
     * @return array of all category names/IDs
     */
    public function getCategories($getAll = false)
    {
        if ($getAll) {
            $sql = 'SELECT * FROM `categories`';
        } else {
            $sql = 'SELECT * FROM `categories` WHERE `parent` = 0';
        }
        
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $categories[] = array('id' => $obj->categoryid,
                'name' => $obj->name, 'parent' => $obj->parent,
                'description' => $obj->description);
        }
        $result->close();
        $this->_clearResults();
        return $categories;
    }
    
    /**
     * Function to get an array of Categories based on a parent ID.
     *
     * @param int $parentId Corresponds to categories.parent
     *
     * @access public
     * @return array
     */
    public function getCategoriesByParentId($parentId = 0)
    {
        $categories = array();
        $this->_escape($parentId);
        $sql    = 'SELECT `categoryid`, `name`, `description`, `parent` '
            . 'FROM `categories` '
            . 'WHERE `parent` = \'' . $parentId . '\' '
            . 'ORDER BY `name` ASC;';
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $categories[$obj->categoryid] = new Category(
                $obj->categoryid, 
                $obj->name, 
                $obj->description,
                $obj->parent
            );
        }
        $result->close();
        return $categories;
    }
    
    /**
     * Determines whether a user is a store owner.
     *
     * @param int    $userId The userID of the user to check.
     * @param object $store  The store to check if user is owner of (optional)
     *
     * @access public
     * @return bool   True on success, false on failure.
     */
    public function isStoreOwner($userId, $store=null)
    {
        if ($store == null) {
            $this->_escape($userId);
            $sql = 'SELECT COUNT(`storeid`) AS `count` '
                . 'FROM `storeowners` '
                . 'WHERE `userid` = ' . $userId . ';';
            $obj = $this->_getObject($sql);
            return ($obj->count > 0) ? true : false;
        } else {
            $store = clone $store;
            $this->_escape($userId);
            $this->_escape($store);
            $sql = 'SELECT COUNT(`storeid`) AS `count` '
                . 'FROM `storeowners` '
                . 'WHERE `userid` = ' . $userId . ''
                . ' AND `storeid` = ' . $store->id . ';';
            $obj = $this->_getObject($sql);
            return ($obj->count > 0) ? true : false;
        }
    }
    
    /**
     * Function to get the total number of stores based on a category ID.
     *
     * @param int $categoryId The categoryId to look up.
     *
     * @access public
     * @return int    The number of stores matching the criteria.
     */
    public function getCategoryCount($categoryId)
    {
        $this->_escape($categoryId);
        
        /** Gets count for current category. */
        $sql   = 'SELECT COUNT(`storeid`) AS `count` '
            . 'FROM `storecategories` '
            . 'WHERE `categoryid` = \'' . $categoryId . '\';';
        $obj   = $this->_getObject($sql);
        $count = $obj->count;
        
        /** Gets count for child categories recursively. */
        $sql    = 'SELECT `categoryid` '
            . 'FROM `categories` '
            . 'WHERE `parent` = \'' . $categoryId . '\';';
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $count += $this->getCategoryCount($obj->categoryid);
        }
        $result->close();
        
        /** Returns the count, including child categories. */
        return $count;
    }
    
    /**
     * Function to get the total number of stores.
     *
     * @access public
     * @return int    The number of stores.
     */
    public function getNumberOfStores()
    {
        $sql = 'SELECT COUNT(`storeid`) AS `count` FROM `stores`;';
        $obj = $this->_getObject($sql);
        return $obj->count;
    }
    
    /**
     * Returns an array of Category objects for a given store ID.
     *
     * @param int $storeId The store ID to look up.
     *
     * @access public
     * @return array  An array of Category objects.
     */
    public function getStoreCategories($storeId)
    {
        $this->_escape($storeId);
        $categories = array();
        $sql        = 'SELECT `categories`.`categoryid`, `categories`.`name`, '
            . '`categories`.`description`, `categories`.`parent` '
            . 'FROM `categories` '
            . 'INNER JOIN `storecategories` '
            . 'ON `categories`.`categoryid` = `storecategories`.`categoryid` '
            . 'WHERE `storecategories`.`storeid` = \'' . $storeId . '\' '
            . 'ORDER BY `categories`.`name` ASC;';
        $result     = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $categories[] = new Category(
                $obj->categoryid, $obj->name, $obj->description, $obj->parent
            );
        }
        $result->close();
        return $categories;
    }
    
    /**
     * Returns an array of Phone objects for a given store ID.
     *
     * @param int $storeId The store ID to look up.
     *
     * @access public
     * @return array  An array of Phone objects.
     */
    public function getStorePhoneNumbers($storeId)
    {
        $this->_escape($storeId);
        $phoneNumbers = array();
        $sql          = 'SELECT `phone`, `description` '
            . 'FROM `storephones` '
            . 'WHERE `storeid` = \'' . $storeId . '\';';
        $result       = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $phoneNumbers[] = new Phone($obj->phone, $obj->description);
        }
        $result->close();
        return $phoneNumbers;
    }
    
    /**
     * Returns an array of Fax objects for a given store ID.
     *
     * @param int $storeId The store ID to look up.
     *
     * @access public
     * @return array  An array of Fax objects.
     */
    public function getStoreFaxNumbers($storeId)
    {
        $this->_escape($storeId);
        $faxNumbers = array();
        $sql        = 'SELECT `fax`, `description` '
            . 'FROM `storefaxes` '
            . 'WHERE `storeid` = \'' . $storeId . '\';';
        $result     = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $faxNumbers[] = new Fax($obj->fax, $obj->description);
        }
        $result->close();
        return $faxNumbers;
    }
    
    /**
     * Returns an array of Email objects for a given store ID.
     *
     * @param int $storeId The store ID to look up.
     *
     * @access public
     * @return array  An array of Email objects.
     */
    public function getStoreEmailAddresses($storeId)
    {
        $this->_escape($storeId);
        $emailAddresses = array();
        $sql            = 'SELECT `email`, `description` '
            . 'FROM `storeemails` '
            . 'WHERE `storeid` = \'' . $storeId . '\';';
        $result         = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $emailAddresses[] = new Email($obj->email, $obj->description);
        }
        $result->close();
        return $emailAddresses;
    }
    
    /**
     * Function to get information on a store based on a store ID.
     *
     * @param int   $storeId   The storeId to look up.
     * @param float $userLat   The user's home latitude.
     * @param float $userLng   The user's home longitude.
     * @param float $getEvents Prevents infinite recursion.
     *
     * @access public
     * @return object A Store object containing all of the information.
     */
    public function getStoreById($storeId, $userLat = null, $userLng = null, 
        $getEvents = false
    ) {
        $this->_escape($storeId);
        
        $sql = 'SELECT * '
            . 'FROM `stores` '
            . 'WHERE `storeid`=' .$storeId
            . ' LIMIT 1;';
        
        $obj = $this->_getObject($sql);
        if (!is_object($obj)) {
            return null;
        }
        
        $categories     = $this->getStoreCategories($storeId);
        $phoneNumbers   = $this->getStorePhoneNumbers($storeId);
        $faxNumbers     = $this->getStoreFaxNumbers($storeId);
        $emailAddresses = $this->getStoreEmailAddresses($storeId);
        
        if ($getEvents) {
            $events = $this->getEventsByStoreId($storeId);
        } else {
            $events = array();
        }
        
        $sql = 'SELECT `stores`.`storeid`, `stores`.`name`, '
            . '`stores`.`description`, `stores`.`dateadded`, '
            . '`stores`.`address`, `stores`.`city`, `stores`.`state`, '
            . '`stores`.`zip`, `stores`.`countryid` AS `country`, '
            . '`stores`.`latitude`, `stores`.`longitude`, '
            . '`storeimages`.`extension`';
        if ($userLat != null && $userLng != null) {
            $sql .= ', HAVERSINE(' . $userLat . ', ' . $userLng . ', '
                . '`stores`.`latitude`, `stores`.`longitude`, \''
                . D_DISTANCE . '\') AS `distance` ';
        } else {
            $sql .= ', NULL as `distance` ';
        }
        $sql .= 'FROM `stores` '
            . 'LEFT OUTER JOIN `storeimages` '
            . 'ON `stores`.`storeid` = `storeimages`.`storeid` '
            . 'WHERE `stores`.`storeid` = \'' . $storeId . '\';';
        
        $obj       = $this->_getObject($sql);
        $imagePath = RELATIVE_PATH . 'images/stores/';
        if ($obj->extension != '') {
            $imagePath .= $obj->storeid . '.' . $obj->extension;
        } else {
            $imagePath .= 'noimage.png';
        }
        
        $this->_clearResults();
        
        return new Store(
            $obj->storeid, $obj->name, $obj->description, 
            strtotime($obj->dateadded), $obj->address, $obj->city, $obj->state,
            $obj->zip, $obj->country, $obj->latitude, $obj->longitude,
            $categories, $phoneNumbers, $faxNumbers, $emailAddresses, 
            $imagePath, $obj->distance, $events
        );
    }
    
    /**
     * Function to create a featured store.
     *
     * @access private
     * @return int    The store ID on success, 0 on failure.
     */
    private function _createFeaturedStore()
    {
        $sql    = 'SELECT `storeid` '
            . 'FROM `stores` '
            . 'WHERE `storeid` NOT IN '
            . '(SELECT `storeid` FROM `featuredstores`) '
            . 'ORDER BY RAND() '
            . 'LIMIT 0,1;';
        $result = $this->_getResult($sql);
            
        /** Check if all available stores have already been featured. */
        if ($result->num_rows == 0) {
            return 0;
        }
        
        /** Add new featured store and return its ID. */
        $obj = $result->fetch_object()
            or trigger_error('Could not fetch object.', E_USER_ERROR);
        $result->close();
        $sql = 'INSERT INTO `featuredstores` (`storeid`, `date`) VALUES '
            . '(\'' . $obj->storeid . '\', CURDATE());';
        $this->_execute($sql);
        return $obj->storeid;
    }
    
    /**
     * Function to get a featured store.
     *
     * @param float $userLat The latitude of the user's home ZIP code.
     * @param float $userLng The longitude of the user's home ZIP code.
     *
     * @access public
     * @return object A Store object containing the day's featured store.
     */
    public function getFeaturedStore($userLat = null, $userLng = null)
    {
        /** Tries to get today's featured store. */
        $sql    = 'SELECT `storeid` '
            . 'FROM `featuredstores` '
            . 'WHERE `date` = CURDATE();';
        $result = $this->_getResult($sql);
        if ($result->num_rows > 0) {
            $obj = $result->fetch_object()
                or trigger_error('Could not fetch object.', E_USER_ERROR);
            $result->close();
            return $this->getStoreById($obj->storeid, $userLat, $userLng);
        }
        
        /**
         * No featured store exists for today, so try to  create a new one 
         * at random from stores that have not yet been picked.
         */
        $storeId = $this->_createFeaturedStore();
        if ($storeId != 0) {
            return $this->getStoreById($storeId, $userLat, $userLng);
        }
        
        /**
         * There are likely no more stores available that haven't yet been
         * featured.  Flush the featured stores table and try again.  This time,
         * if the lookup fails, return false.
         */
        $this->_flush('featuredstores');
        $storeId = $this->_createFeaturedStore();
        if ($storeId != 0) {
            return $this->getStoreById($storeId, $userLat, $userLng);
        } else {
            return false;
        }
    }
    
    /**
     * Function to get a category based on a category ID.
     *
     * @param int $categoryId The category ID to look up.
     *
     * @access public
     * @return object A Category object corresponding to the ID.
     */
    public function getCategoryById($categoryId)
    {
        $this->_escape($categoryId);
        if ($categoryId == 0) {
            return new Category(0, 'All Categories');
        }
        $sql    = 'SELECT `categoryid`, `name`, `description`, `parent` '
            . 'FROM `categories` '
            . 'WHERE `categoryid` = \'' . $categoryId . '\';';
        $result = $this->_getResult($sql);
        if ($result->num_rows == 0) {
            return new Category(0, 'All Categories');
        }
        $obj = $result->fetch_object()
            or trigger_error('Could not fetch object.', E_USER_ERROR);
        $result->close();
        return new Category(
            $obj->categoryid, $obj->name, $obj->description, $obj->parent
        );
    }
    
    /**
     * Function to get an array of categories based on a category ID and a page.
     *
     * @param int   $categoryId The category ID to look up.
     * @param int   $storePage  The pagination to use in limit values.
     * @param float $userLat    The latitude of the user's home ZIP code.
     * @param float $userLng    The longitude of the user's home ZIP code.
     *
     * @access public
     * @return array  An array of Store objects corresponding to the category.
     */
    public function getStoresByCategoryId($categoryId, $storePage = 0,
        $userLat = null, $userLng = null
    ) {
        $this->_escape($categoryId);
        $this->_escape($storePage);
        $this->_escape($userLat);
        $this->_escape($userLng);
        $stores = array();
        $sql    = 'SELECT `stores`.`storeid`';
        if ($userLat != null && $userLng != null) {
            $sql .= ', HAVERSINE(' . $userLat . ', ' . $userLng . ', '
                . '`stores`.`latitude`, `stores`.`longitude`, \''
                . D_DISTANCE . '\') AS `distance`';
        }
        $sql .= ' FROM `stores` ';
        if ($categoryId != 0) {
            $sql .= 'INNER JOIN `storecategories` '
                . 'ON `stores`.`storeid` = `storecategories`.`storeid` '
                . 'WHERE `storecategories`.`categoryid` = \''
                . $categoryId . '\' ';
        }
        $sql .= 'ORDER BY ';
        if ($userLat != null && $userLng != null) {
            $sql .= '`distance` ASC, ';
        }
        $sql   .= '`stores`.`name` ASC '
            . 'LIMIT ' . (($storePage - 1) * STORES_PER_PAGE) . ','
            . STORES_PER_PAGE . ';';
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $stores[$obj->storeid] = $this->getStoreById(
                $obj->storeid, $userLat, $userLng
            );
        }
        $result->close();
        return $stores;
    }
    
    /**
     * Function to get page content based on a content ID.
     *
     * @param string $contentId The content ID to pull.
     * @param int    $indent    The number of spaces to indent the HTML.
     *
     * @access public
     * @return string Content from content.content or false on failure.
     */
    public function getContent($contentId, $indent = 0)
    {
        $this->_escape($contentId);
        $spacing = str_pad('', $indent, ' ', STR_PAD_LEFT);
        $sql     = 'SELECT `content` '
            . 'FROM `content` '
            . 'WHERE `contentid` = \'' . $contentId . '\';';
        $result  = $this->_getResult($sql);
        if ($result->num_rows == 0) {
            return false;
        }
        $obj = $result->fetch_object()
            or trigger_error('Could not fetch object.', E_USER_ERROR);
        $result->close();
        $content = $spacing . $obj->content;
        $content = str_ireplace('<sitename>', SITENAME, $content);
        $content = escapeRaw($content);
        $content = preg_replace('/\n/', "\n$spacing", $content);
        return $content;
    }

    /**
     * Function to update page content based on a content ID.
     *
     * @param string $contentId The content ID to pull.
     * @param string $content   The updated content.
     *
     * @access public
     * @return mixed True on success, error message on failure.
     */
    public function updateContent($contentId, $content)
    {
        $content = stripslashes($content); /** quotes build slashes otherwise? */
        $this->_escape($contentId);
        $this->_escape($content);
        
        $sql = 'UPDATE `content` SET `content` = \'' . $content
            . '\' WHERE `contentid` = \'' . $contentId . '\';';
        
        $this->_mysqli->real_query($sql)
            or trigger_error($this->_mysqli->error);
        
        if ($this->_mysqli->affected_rows != 1) {
            return 'Failed to update `' . $contentId . '`';
        }
        
        return true;
    }
    
    /**
     * Function to get user information based on an email address.
     *
     * @param string $email The email address to look up.
     *
     * @access public
     * @return mixed  A User object on success or false on failure.
     */
    public function getUserByEmail($email)
    {
        $this->_escape($email);
        $sql    = 'SELECT `userid` '
            . 'FROM `users` '
            . 'WHERE `email` = \'' . $email . '\';';
        $result = $this->_getResult($sql);
        
        /** If no user is found matching the email provided, return false. */
        if ($result->num_rows == 0) {
            return false;
        }
        
        /** Return the user object based on the user ID. */
        $obj = $result->fetch_object();
        $result->close();
        return $this->getUserById($obj->userid);
    }
    
    /**
     * Function to get a ZIP code object based on a ZIP code.
     *
     * @param string $zip The ZIP code to look up.
     *
     * @access public
     * @return object A Zip object populated with information from the DB.
     */
    public function getZipByZip($zip)
    {
        if ($zip == '') {
            return false;
        }
        $this->_escape($zip);
        $sql    = 'SELECT `zip`, `city`, `state`, `latitude`, `longitude` '
            . 'FROM `zipcodes` '
            . 'WHERE `zip` = \'' . $zip . '\' '
            . 'LIMIT 0,1;';
        $result = $this->_getResult($sql);
        if ($result->num_rows == 0) {
            return false;
        }
        $obj = $result->fetch_object();
        return new Zip($obj->zip, $obj->city, $obj->state, $obj->latitude,
            $obj->longitude);
    }
    
    /**
     * Function to get an array of stores based on a search query.
     *
     * @param string $query   The search query to use.
     * @param int    $page    The page number to use when limiting the query.
     * @param float  $userLat The latitude of the user's home zip code.
     * @param float  $userLng The longitude of the user's home zip code.
     * @param int    $dist    The maximum distance to search.
     *
     * @access public
     * @return array  An array of Store objects.
     */
    public function getStoresByQuery($query, $page, $userLat, $userLng, $dist)
    {
        /** Creates an array to contain the return values. */
        $storeInfo = array();
        
        /** Escapes values. */
        $this->_escape($query);
        $this->_escape($page);
        $this->_escape($userLat);
        $this->_escape($userLng);
        $this->_escape($dist);
        
        /** Calculates the limit value. */
        $min = ($page - 1) * STORES_PER_PAGE;
        
        /** Gets the total number of results. */
        $sql = 'SELECT DISTINCT `stores`.`storeid` ' 
            . 'FROM `stores` '
            . 'LEFT OUTER JOIN `storecategories` '
            . 'ON `stores`.`storeid` = `storecategories`.`storeid` '
            . 'INNER JOIN `categories` '
            . 'ON `storecategories`.`categoryid` = `categories`.`categoryid` '
            . 'LEFT OUTER JOIN `storeemails` '
            . 'ON `stores`.`storeid` = `storeemails`.`storeid` '
            . 'LEFT OUTER JOIN `storefaxes` '
            . 'ON `stores`.`storeid` = `storefaxes`.`storeid` '
            . 'LEFT OUTER JOIN `storephones` '
            . 'ON `stores`.`storeid` = `storephones`.`storeid` '
            . 'WHERE (`stores`.`name` LIKE \'%' . $query . '%\' '
            . 'OR `stores`.`description` LIKE \'%' . $query . '%\' '
            . 'OR `stores`.`address` LIKE \'%' . $query . '%\' '
            . 'OR `stores`.`city` LIKE \'%' . $query . '%\' '
            . 'OR `stores`.`state` LIKE \'%' . $query . '%\' '
            . 'OR `stores`.`zip` LIKE \'%' . $query . '%\' '
            . 'OR `storephones`.`phone` LIKE \'%' . $query . '%\' '
            . 'OR `storefaxes`.`fax` LIKE \'%' . $query . '%\' '
            . 'OR `storeemails`.`email` LIKE \'%' . $query . '%\' '
            . 'OR `categories`.`name` LIKE \'%' . $query . '%\') '
            . 'AND HAVERSINE(' . $userLat . ', ' . $userLng . ', '
            . '`stores`.`latitude`, `stores`.`longitude`, \''
            . D_DISTANCE . '\') <= ' . $dist . ';'; 
        $result = $this->_getResult($sql);
        $storeInfo['totalResults'] = $result->num_rows;
        
        /** Gets the results limited by the total stores per page & current page. */
        $sql = 'SELECT DISTINCT `stores`.`storeid`, ' 
            . 'HAVERSINE(' . $userLat . ', ' . $userLng . ', '
            . '`stores`.`latitude`, `stores`.`longitude`, \''
            . D_DISTANCE . '\') AS `distance` '
            . 'FROM `stores` '
            . 'LEFT OUTER JOIN `storecategories` '
            . 'ON `stores`.`storeid` = `storecategories`.`storeid` '
            . 'INNER JOIN `categories` '
            . 'ON `storecategories`.`categoryid` = `categories`.`categoryid` '
            . 'LEFT OUTER JOIN `storeemails` '
            . 'ON `stores`.`storeid` = `storeemails`.`storeid` '
            . 'LEFT OUTER JOIN `storefaxes` '
            . 'ON `stores`.`storeid` = `storefaxes`.`storeid` '
            . 'LEFT OUTER JOIN `storephones` '
            . 'ON `stores`.`storeid` = `storephones`.`storeid` '
            . 'WHERE (`stores`.`name` LIKE \'%' . $query . '%\' '
            . 'OR `stores`.`description` LIKE \'%' . $query . '%\' '
            . 'OR `stores`.`address` LIKE \'%' . $query . '%\' '
            . 'OR `stores`.`city` LIKE \'%' . $query . '%\' '
            . 'OR `stores`.`state` LIKE \'%' . $query . '%\' '
            . 'OR `stores`.`zip` LIKE \'%' . $query . '%\' '
            . 'OR `storephones`.`phone` LIKE \'%' . $query . '%\' '
            . 'OR `storefaxes`.`fax` LIKE \'%' . $query . '%\' '
            . 'OR `storeemails`.`email` LIKE \'%' . $query . '%\' '
            . 'OR `categories`.`name` LIKE \'%' . $query . '%\') '
            . 'AND HAVERSINE(' . $userLat . ', ' . $userLng . ', '
            . '`stores`.`latitude`, `stores`.`longitude`, \''
            . D_DISTANCE . '\') <= ' . $dist . ' ' 
            . 'ORDER BY `distance` ASC '
            . 'LIMIT ' . $min . ',' . STORES_PER_PAGE . ';';
        $result = $this->_getResult($sql);
        
        /** Builds the stores array and returns it. */
        $stores = array();
        while ($obj = $result->fetch_object()) {
            $stores[] = $this->getStoreById($obj->storeid, $userLat, $userLng);
        }
        $storeInfo['stores'] = $stores;
        return $storeInfo;
    }
    
    /**
     * Function to update user information.
     *
     * @param object $oldUser containing current data.
     * @param object $newUser containing updated data.
     *
     * @access public
     * @return mixed True on success or an error message.
     */
    public function updateUserInfo($oldUser, $newUser)
    {
        /** Objects act as if passed by reference, so clone them for safety. */
        if ($oldUser != null) {
            $oldUser = clone $oldUser;
        }
        if ($newUser != null) {
            $newUser = clone $newUser;
        }
        
        /** Sanitize input for SQL. */
        $this->_escape($oldUser);
        $this->_escape($newUser);
        $this->_execute("START TRANSACTION;");
        
        /** Process name if it needs to be updated. */
        if ($oldUser->name != $newUser->name) {
            if (strlen($newUser->name) > L_NAME) {
                return 'The name cannot exceed ' . L_NAME . ' characters.';
            }
            
            /** INSERT, DELETE, or UPDATE name. */
            if ($oldUser->name == '') {
                $sql = 'INSERT INTO `usernames` (`userid`, `name`) VALUES '
                    . '(\'' . $oldUser->id . '\', \'' . $newUser->name . '\')';
                $this->_mysqli->real_query($sql)
                    or trigger_error($this->_mysqli->error, E_USER_ERROR);
                
            } elseif ($newUser->name == '') {
                $sql = 'DELETE FROM `usernames` WHERE `userid` = \''
                    .$oldUser->id . '\' LIMIT 1';
                $this->_mysqli->real_query($sql)
                    or trigger_error($this->_mysqli->error, E_USER_ERROR);
                
            } else {
                $sql = 'UPDATE `usernames` SET `name` = \'' .$newUser->name
                    . '\' WHERE `userid` = \'' .$oldUser->id. '\' LIMIT 1';
                $this->_mysqli->real_query($sql)
                    or trigger_error($this->_mysqli->error, E_USER_ERROR);
            }
            
            if ($this->_mysqli->affected_rows != 1) {
                return 'Failed to update name.';;
            }
        }
        
        /** Process ZIP if it needs to be updated. */
        if ($oldUser->zip->zip != $newUser->zip->zip) {
            if (strlen($newUser->zip->zip) > L_ZIP
                || strlen($newUser->zip->zip) < 3
            ) {
                return 'The zip code is not valid.';
            }
            
            $sql = 'UPDATE `users` SET `zip` = \'' .$newUser->zip->zip
                . '\' WHERE `userid` = \'' .$oldUser->id. '\' LIMIT 1';
            $this->_mysqli->real_query($sql)
                or trigger_error($this->_mysqli->error, E_USER_ERROR);
            
            if ($this->_mysqli->affected_rows != 1) {
                return 'Failed to update ZIP code.';
            }
        }
        
        /** Process email if it needs to be updated. */
        if ($oldUser->email != $newUser->email) {
            /** Determines if the email provided is valid. */
            if (!validEmail($newUser->email)) {
                return 'The email address you provided is not valid.';
            }
            if (strlen($newUser->email) > L_EMAIL) {
                return 'The email address cannot exceed ' . L_EMAIL
                    . ' characters.';
            }
            
            /** Determines if the email provided is already in use. */
            if ($this->db->getUserByEmail($newUser->email) !== false) {
                return 'The email address you provided is already in use.';
            }
        
            /** UPDATE database entry. */
            $sql = 'UPDATE `users` SET `email` = \'' .$newUser->email
                . '\' WHERE `userid` = \'' .$oldUser->id. '\' LIMIT 1';
            $this->_mysqli->real_query($sql)
                or trigger_error($this->_mysqli->error, E_USER_ERROR);
            
            if ($this->_mysqli->affected_rows != 1) {
                return 'Failed to update password.';
            }
        }
        
        $this->_mysqli->commit();
        return true;
    }
    
    /**
     * Function to update user password.
     *
     * @param object $user        containing user data.
     * @param object $oldPassword User's current password.
     * @param object $newPassword User's new password.
     *
     * @access public
     * @return mixed True on success or an error message.
     */
    public function updateUserPassword($user, $oldPassword, $newPassword)
    {
        /** Objects act as if passed by reference, so clone them for safety. */
        if ($user != null) {
            $user = clone $user;
        }
        
        /** Sanitize input for SQL. */
        $this->_escape($oldPassword);
        $this->_escape($newPassword);
        $this->_escape($user);
        
        /** Check if new password is valid. */
        if (strlen($newPassword) > L_PASSWORD) {
            return 'The password cannot exceed ' . L_PASSWORD . ' characters.';
        }
        
        /** Execute SQL. */
        $sql = 'UPDATE `users` SET `password` = \'' .makeHash($newPassword)
            . '\' WHERE `userid` = \'' .$user->id. '\''
            . ' AND `password` = \'' .makeHash($oldPassword). '\' LIMIT 1';
        $this->_mysqli->real_query($sql)
            or trigger_error($this->_mysqli->error, E_USER_ERROR);
        
        /** Error check. */
        if ($this->_mysqli->affected_rows != 1) {
            return 'Failed to update password.';
        }
        
        return true;
    }
    
    /**
     * Function to update a user phone number.
     *
     * @param object $user  containing user data.
     * @param object $phone containing phone data.
     *
     * @access public
     * @return mixed True on success or an error message.
     */
    public function updateUserPhone($user, $phone)
    {
        /** Objects act as if passed by reference, so clone them for safety. */
        if ($phone != null) {
            $phone = clone $phone;
        }
        if ($user != null) {
            $user = clone $user;
        }
        
        /** Sanitize input. */
        $this->_escape($user);
        $this->_escape($phone);
    
        /** Validate input. */
        if (strlen($phone->number) > L_PHONE) {
            return 'The phone number cannot exceed ' . L_PHONE 
                . 'characters.';
        }
        if (strlen($phone->description) > L_CONTACTDESC) {
            return 'The phone description cannot exceed ' . L_CONTACTDESC
                . ' characters.';
        }
        
        /** Execute SQL. */
        $sql = 'UPDATE `userphones` '
            . 'SET `phone`= \'' .$phone->number. '\' '
            . 'WHERE `userid`=\'' .$user->id. '\' AND '
            . '`description`=\'' . $phone->description . '\' LIMIT 1;';
        $this->_execute($sql);
        
        return true;
    }
    
    /**
     * Function to add a user phone.
     *
     * @param object $user  containing user data.
     * @param object $phone containing phone data.
     *
     * @access public
     * @return mixed True on success or an error message.
     */
    public function addUserPhone($user, $phone)
    {
        /** Objects act as if passed by reference, so clone them for safety. */
        if ($phone != null) {
            $phone = clone $phone;
        }
        if ($user != null) {
            $user = clone $user;
        }
        
        /** Sanitize input. */
        $this->_escape($user);
        $this->_escape($phone);
    
        /** Validate input. */
        if (strlen($phone->number) > L_PHONE) {
            return 'The phone number cannot exceed ' . L_PHONE 
                . ' characters.';
        }
        if (strlen($phone->description) > L_CONTACTDESC) {
            return 'The phone description cannot exceed ' . L_CONTACTDESC
                . ' characters.';
        }
        
        /** Execute SQL. */
        $sql = 'INSERT INTO `userphones` '
            . '(`userid`, `phone`, `description`) VALUES '
            . '(\'' .$user->id. '\', '
            . '\'' .$phone->number. '\', '
            . '\''  .$phone->description. '\');';
        $this->_execute($sql);
        
        return true;
    }
    
    /**
     * Function to delete a user phone.
     *
     * @param object $user  containing user data.
     * @param object $phone containing phone data.
     *
     * @access public
     * @return mixed True on success or an error message.
     */
    public function deleteUserPhone($user, $phone)
    {
        /** Objects act as if passed by reference, so clone them for safety. */
        if ($phone != null) {
            $phone = clone $phone;
        }
        if ($user != null) {
            $user = clone $user;
        }
        
        /** Sanitize input. */
        $this->_escape($user);
        $this->_escape($phone);
    
        /** Validate input. */
        if (strlen($phone->number) > L_PHONE) {
            return 'The phone number cannot exceed ' . L_PHONE 
                . 'characters.';
        }
        if (strlen($phone->description) > L_CONTACTDESC) {
            return 'The phone description cannot exceed ' . L_CONTACTDESC
                . ' characters.';
        }
        
        /** Execute SQL. */
        $sql = 'DELETE FROM `userphones` WHERE '
            . '`userid`=\'' . $user->id . '\' AND '
            . '`phone`=\'' . $phone->number . '\' AND '
            . '`description`=\'' . $phone->description . '\' '
            . 'LIMIT 1;';
        $this->_execute($sql);
        
        return true;
    }
    
    /**
     * Function to delete user account.
     *
     * @param object $user containing user data.
     *
     * @access public
     * @return bool True on success, or triggers error
     */
    public function deleteUserAccount($user)
    {
        $this->_escape($user);
        $sql = 'DELETE FROM `users` WHERE `userid`=' . $user->id. ';' . "\n";
        
        return $this->_executeTransaction($sql);
    }

    /**
     * Function to build array of country names/IDs from database.
     *
     * @access public
     * @return array of all country names/IDs
     */
    public function getCountries()
    {
        $sql    = 'SELECT * FROM `countries`';
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $countries[] = array('id' => $obj->countryid,
                'name' => $obj->name);
        }
        $result->close();
        return $countries;
    }

    /**
     * Function to add a store to the database.
     *
     * @param object $user  User object
     * @param object $store Store object
     *
     * @access public
     * @return mixed True on success, error message on fail.
     */
    public function createStore($user, $store)
    {
        /** Objects act as if passed by reference, so clone them for safety. */
        if ($store != null) {
            $store = clone $store;
        }
        if ($user != null) {
            $user = clone $user;
        }
        
        $this->_escape($user);
        $this->_escape($store);

        /** Validate Store variables. */
        if (strlen($store->name) > L_STORENAME) {
            return 'Store name is not valid.';
        }
        if (strlen($store->address) > L_STOREADDR) {
            return 'Store address is not valid.';
        }
        if (strlen($store->city) > L_STOREADDR) {
            return 'City name is not valid.';
        }
        
        if ($store->id == '') {
            $sql = 'INSERT INTO `stores` (`name` , `description` , '
                . '`dateadded` , `address` , `city` , `state` , `zip` , '
                . '`countryid` , `latitude` , `longitude`) '
                . 'VALUES (\''.$store->name.'\', \''.$store->description.'\', '
                . 'NOW(), \''.$store->address.'\', \''.$store->city.'\', '
                . '\''.$store->state.'\', \''.$store->zip.'\', '.$store->country
                . ', '.$store->latitude.', '.$store->longitude.');' . "\n";
            
        } else {
            $sql = 'INSERT INTO `stores` (`storeid` , `name` , `description` , '
                . '`dateadded` , `address` , `city` , `state` , `zip` , '
                . '`countryid` , `latitude` , `longitude`) '
                . 'VALUES ('.$store->id.', \''.$store->name.'\', \''
                . $store->description.'\', NOW(), \''.$store->address.'\', \''
                . $store->city.'\', \''.$store->state.'\', \''.$store->zip
                . '\', '.$store->country.', '.$store->latitude.', '
                . $store->longitude.');' . "\n";
        }
        
        /** Insert each phone. */
        foreach ($store->phoneNumbers as $phone) {
            if (strlen($phone->number) > L_PHONE) {
                return 'Phone number is not valid.';
            }
            
            $sql .= 'INSERT INTO `storephones` (`storeid`, `phone`, '
                . '`description`) '
                . 'VALUES ('
                . '(SELECT `storeid` FROM `stores` WHERE `name` = \''
                . $store->name.'\' AND `description` = \''.$store->description
                .'\' AND `address` = \''.$store->address.'\' ORDER BY `storeid`'
                . ' DESC LIMIT 1)'
                . ', \''.$phone->number.'\', \''.$phone->description.'\');'
                . "\n";
        }

        /** Insert each email. */
        foreach ($store->emailAddresses as $email) {
            if (strlen($email->address) > L_EMAIL
                || !validEmail($email->address)
            ) {
                return 'Email is not valid.';
            }
            $sql .= 'INSERT INTO `storeemails` (`storeid`, `email`, '
                . '`description`) '
                . 'VALUES ('
                . '(SELECT `storeid` FROM `stores` WHERE `name` = \''
                . $store->name.'\' AND `description` = \''.$store->description
                .'\' AND `address` = \''.$store->address.'\' ORDER BY `storeid`'
                . ' DESC LIMIT 1)'
                . ', \''.$email->address.'\', \''.$email->description.'\');'
                . "\n";
        }
        
        /** Insert each fax. */
        foreach ($store->faxNumbers as $fax) {
            if (strlen($fax->number) > L_PHONE) {
                return 'Fax number is not valid.';
            }
            $sql .= 'INSERT INTO `storefaxes` (`storeid`, `fax`, '
                . '`description`) '
                . 'VALUES ('
                . '(SELECT `storeid` FROM `stores` WHERE `name` = \''
                . $store->name.'\' AND `description` = \''.$store->description
                .'\' AND `address` = \''.$store->address.'\' ORDER BY `storeid`'
                . ' DESC LIMIT 1)'
                . ', \''.$fax->number.'\', \''.$fax->description.'\');' . "\n";
        }
    
        /** Insert each category. */
        foreach ($store->categories as $cat) {
            $sql .= 'INSERT INTO `storecategories` (`storeid`, `categoryid`) '
                . 'VALUES ('
                . '(SELECT `storeid` FROM `stores` WHERE `name` = \''
                . $store->name.'\' AND `description` = \''.$store->description
                .'\' AND `address` = \''.$store->address.'\' ORDER BY `storeid`'
                . ' DESC LIMIT 1),'
                . $cat->id.');' . "\n";
        }

        /** Create link between each store and event. */
        foreach ($store->events as $event) {
            $sql .= 'INSERT INTO `storeevents` (`storeid` , `eventid`)'
                . 'VALUES (' . "\n"
                . '(SELECT `storeid` FROM `stores` WHERE `name` = \''
                . $store->name.'\' AND `description` = \''
                . $store->description.'\' AND `address` = \''
                . $store->address.'\' '
                . 'ORDER BY `storeid` DESC LIMIT 1)' . "\n"
                . ', '.$event->id.');' . "\n";
        }
        
        /** Insert store owner. */
        $sql .= 'INSERT INTO `storeowners` (`storeid` , `userid`) '
            . 'VALUES ('
            . '(SELECT `storeid` FROM `stores` WHERE `name` = \''
            . $store->name.'\' AND `description` = \''.$store->description.'\''
            . ' AND `address` = \''.$store->address.'\' ORDER BY `storeid` '
            . 'DESC LIMIT 1)'
            . ', '.$user->id.');' . "\n";

        $this->_executeTransaction($sql);
        return true;
    }

    /**
     * Function to delete store.
     *
     * @param object $store containing store data.
     *
     * @access public
     * @return bool True on success, or triggers error
     */
    public function deleteStore($store)
    {
        $this->_escape($store);
        $sql = 'DELETE FROM `stores` WHERE `storeid`=' . $store->id . ';'
             . "\n";
        
        return $this->_executeTransaction($sql);
    }

    /**
     * Function to add an Event to the database.
     *
     * @param object $user  User object
     * @param object $event Event object
     *
     * @access public
     * @return mixed True on success, error message on fail.
     */
    public function createEvent($user, $event)
    {
        /** Objects act as if passed by reference, so clone them for safety. */
        if ($event != null) {
            $event = clone $event;
        }
        if ($user != null) {
            $user = clone $user;
        }
        
        $this->_escape($user);
        $this->_escape($event);

        /** Validate Store variables. */
        if (strlen($event->name) > L_STORENAME) {
            return 'Event name is not valid.';
        }
        if (strlen($event->address) > L_STOREADDR) {
            return 'Event address is not valid.';
        }
        if (strlen($event->city) > L_STOREADDR) {
            return 'City name is not valid.';
        }
        
        $sql = '';
        if ($event->id == '') {
            $sql .= 'INSERT INTO `events` (`name` , `description` , '
                . '`dateadded` , `starttime` , `endtime` , `address` , `city` ,'
                . ' `state` , `zip` , `countryid` , `latitude` , `longitude`) '
                . 'VALUES (\''.$event->name.'\', \''.$event->description.'\', '
                . 'NOW(), \''.strftime('%Y-%m-%d %H:%M:%S', $event->startTime)
                . '\', \''.strftime('%Y-%m-%d %H:%M:%S', $event->endTime)
                . '\', \''.$event->address.'\', \''.$event->city.'\', \''
                . $event->state.'\', \''.$event->zip->zip.'\', '
                . $event->country.', '.$event->latitude.', '
                . $event->longitude.');' . "\n";
            
        } else {
            $sql .= 'INSERT INTO `events` (`eventid` , `name` , `description` ,'
                . ' `dateadded` , `starttime` , `endtime` , `address` , `city` ,'
                . ' `state` , `zip` , `countryid` , `latitude` , `longitude`) '
                . 'VALUES ('.$event->id.', \''.$event->name.'\', \''
                . $event->description.'\', NOW(), \''
                . strftime('%Y-%m-%d %H:%M:%S', $event->startTime)
                . '\', \''.strftime('%Y-%m-%d %H:%M:%S', $event->endTime)
                . '\', \''.$event->address.'\', \''.$event->city.'\', \''
                . $event->state.'\', \''.$event->zip->zip.'\', '
                . $event->country.', '.$event->latitude.', '
                . $event->longitude.');' . "\n";
        }

        /** Create link between each store and event. */
        foreach ($event->stores as $store) {
            $sql .= 'INSERT INTO `storeevents` (`eventid` , `storeid`)'
                . 'VALUES (' . "\n"
                . '(SELECT `eventid` FROM `events` WHERE `name` = \''
                . $event->name.'\' AND `description` = \''
                . $event->description.'\' AND `address` = \''
                . $event->address.'\' AND `starttime` = \''
                . strftime('%Y-%m-%d %H:%M:%S', $event->startTime)
                . '\' AND `endtime` = \''
                . strftime('%Y-%m-%d %H:%M:%S', $event->endTime)
                . '\' ORDER BY `eventid` DESC LIMIT 1)' . "\n"
                . ', '.$store->id.');' . "\n";
        }
        
        /** Insert Event owner. */
        $sql .= 'INSERT INTO `eventowners` (`eventid` , `userid`) '
            . 'VALUES ('
            . '(SELECT `eventid` FROM `events` WHERE `name` = \''
            . $event->name.'\' AND `description` = \''.$event->description.'\''
            . ' AND `address` = \''.$event->address.'\' ORDER BY `eventid` '
            . 'DESC LIMIT 1)'
            . ', '.$user->id.');' . "\n";

        return $this->_executeTransaction($sql);
    }

    /**
     * Function to delete event.
     *
     * @param object $event containing event data.
     *
     * @access public
     * @return bool True on success, or triggers error
     */
    public function deleteEvent($event)
    {
        $this->_escape($event);
        $sql = 'DELETE FROM `events` WHERE `eventid`=' . $event->id . ';'
            . "\n";
        return $this->_executeTransaction($sql);
    }

    /**
     * Function to link a store with an event.
     *
     * @param object $storeId of store to link.
     * @param object $eventId of event to link.
     *
     * @access public
     * @return bool True on success, or triggers error
     */
    public function linkStoreAndEvent($storeId, $eventId)
    {
        $this->_escape($eventId);
        $this->_escape($storeId);
        $sql = 'REPLACE INTO `storeevents` (`eventid` , `storeid`) '
            . 'VALUES ('.$eventId.', '.$storeId.');' . "\n";
        
        $this->_execute($sql);
        return true;
    }

    /**
     * Function to unlink a store with an event.
     *
     * @param object $storeId of store to unlink.
     * @param object $eventId of event to unlink.
     *
     * @access public
     * @return bool True on success, or triggers error
     */
    public function unlinkStoreAndEvent($storeId, $eventId)
    {
        $this->_escape($eventId);
        $this->_escape($storeId);
        $sql = 'DELETE FROM `storeevents` WHERE `storeid`='.$storeId
            . ' AND `eventid`='.$eventId.';';
        
        $this->_execute($sql);
        return true;
    }

    /**
     * Function to get information on an event based on an event ID.
     *
     * @param int   $eventId   The eventId to look up.
     * @param float $userLat   The user's home latitude.
     * @param float $userLng   The user's home longitude.
     * @param float $getStores Used to prevent infinite recursion.
     *
     * @access public
     * @return object An Event object containing all of the information.
     */
    public function getEventById($eventId, $userLat = null, $userLng = null,
        $getStores = false
    ) {
        $this->_escape($eventId);
        
        $sql = 'SELECT `events`.`eventid`, `events`.`name`, '
            . '`events`.`description`, `events`.`dateadded`, '
            . '`events`.`starttime`, `events`.`endtime`, '
            . '`events`.`address`, `events`.`city`, `events`.`state`, '
            . '`events`.`zip`, `events`.`countryid` AS `country`, '
            . '`events`.`latitude`, `events`.`longitude`';
        if ($userLat != null && $userLng != null) {
            $sql .= ', HAVERSINE(' . $userLat . ', ' . $userLng . ', '
                . '`events`.`latitude`, `events`.`longitude`, \''
                . D_DISTANCE . '\') AS `distance` ';
        } else {
            $sql .= ', NULL as `distance` ';
        }
        $sql .= 'FROM `events` '
            . 'WHERE `events`.`eventid` = \'' . $eventId . '\';';

        if ($getStores) {
            $stores = $this->getStoresByEventId($eventId);
        } else {
            $stores = array();
        }
        
        $obj = $this->_getObject($sql);
        if (!is_object($obj)) {
            return null;
        }
        
        $imagePath = RELATIVE_PATH . 'images/stores/noimage.png';

        $sql = 'SELECT `storeid` FROM `storeevents` WHERE `eventid` = '
            . $eventId;
        $this->_mysqli->real_query($sql)
            or trigger_error($this->_mysqli->error, E_USER_ERROR);

        $this->_clearResults();
        
        return new Event(
            $obj->eventid, $obj->name, $obj->description, 
            strtotime($obj->dateadded), strtotime($obj->starttime),
            strtotime($obj->endtime), $obj->address, $obj->city, $obj->state,
            $this->getZipByZip($obj->zip), $obj->country, $obj->latitude,
            $obj->longitude, $imagePath, $obj->distance, $stores
        );
    }

    /**
     * Function to get an array of event based on page number.
     *
     * @param int   $eventPage The pagination to use in limit values.
     * @param float $userLat   The latitude of the user's home ZIP code.
     * @param float $userLng   The longitude of the user's home ZIP code.
     *
     * @access public
     * @return array  An array of Event objects.
     */
    public function getEvents($eventPage = 0, $userLat = null, $userLng = null)
    {
        $this->_escape($storePage);
        $this->_escape($userLat);
        $this->_escape($userLng);
        $events = array();
        
        $sql = 'SELECT `events`.`eventid`';
        if ($userLat != null && $userLng != null) {
            $sql .= ', HAVERSINE(' . $userLat . ', ' . $userLng . ', '
                . '`events`.`latitude`, `events`.`longitude`, \''
                . D_DISTANCE . '\') AS `distance`';
        }
        $sql .= ' FROM `events` ORDER BY ';
        if ($userLat != null && $userLng != null) {
            $sql .= '`distance` ASC, ';
        }
        $sql .= '`events`.`name` ASC '
            . 'LIMIT ' . (($eventPage - 1) * STORES_PER_PAGE) . ','
            . STORES_PER_PAGE . ';';
        
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $events[$obj->eventid] = $this->getEventById(
                $obj->eventid, $userLat, $userLng
            );
        }
        
        $result->close();
        return $events;
    }

    /**
     * Function to get the total number of events.
     *
     * @access public
     * @return int    The number of events.
     */
    public function getNumberOfEvents()
    {
        $sql = 'SELECT COUNT(`eventid`) '
            . 'AS `count` '
            . 'FROM `events`;';
        
        $obj = $this->_getObject($sql);
        return $obj->count;
    }

    /**
     * Determines whether a user is an event owner.
     *
     * @param int    $userId The userID of the user to check.
     * @param object $event  The event to check if user is owner of (optional)
     *
     * @access public
     * @return bool   True on success, false on failure.
     */
    public function isEventOwner($userId, $event=null)
    {
        /** Objects act as if passed by reference, so clone them for safety. */
        if ($event != null) {
            $event = clone $event;
        }
        
        if ($event == null) {
            $this->_escape($userId);
            $sql = 'SELECT COUNT(`eventid`) AS `count` '
                . 'FROM `eventowners` '
                . 'WHERE `userid` = ' . $userId . ';';
            $obj = $this->_getObject($sql);
            return ($obj->count > 0) ? true : false;
        } else {
            $this->_escape($userId);
            $this->_escape($event);
            $sql = 'SELECT COUNT(`eventid`) AS `count` '
                . 'FROM `eventowners` '
                . 'WHERE `userid` = ' . $userId . ''
                . ' AND `eventid` = ' . $event->id . ';';
            $obj = $this->_getObject($sql);
            return ($obj->count > 0) ? true : false;
        }
    }

    /**
     * Get events for store.
     *
     * @param int $storeId ID of Store to get events for.
     *
     * @access public
     * @return array  Events for store.
     */
    public function getEventsByStoreId($storeId)
    {
        $this->_escape($storeId);
        
        $sql = 'SELECT `eventid` FROM `storeevents` '
            . 'WHERE `storeid` = '.$storeId;

        $events = array();
        
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $events[] = $this->getEventById($obj->eventid);
        }
        $result->close();
        $this->_clearResults();
        
        return $events;
    }

    /**
     * Get stores for event.
     *
     * @param int $eventId ID of event to get stores for.
     *
     * @access public
     * @return array  Stores for event.
     */
    public function getStoresByEventId($eventId)
    {
        $this->_escape($eventId);
        
        $sql = 'SELECT `storeid` FROM `storeevents` '
            . 'WHERE `eventid` = '.$eventId;

        $stores = array();
        
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $stores[] = $this->getStoreById($obj->storeid);
        }
        $result->close();
        $this->_clearResults();

        return $stores;
    }

    /**
     * Function to get an array of users based on a page.
     *
     * @param int $page The pagination to use in limit values.
     *
     * @access public
     * @return array  An array of User objects corresponding to the page.
     */
    public function getUsersByPage($page = 1)
    {
        $this->_escape($page);
        $users = array();
        $sql = 'SELECT `userid` FROM `users` '
            . 'ORDER BY `userid` ASC '
            . 'LIMIT ' . (($page - 1) * USERS_PER_PAGE) . ','
            . USERS_PER_PAGE . ';';
        $result = $this->_getResult($sql);
        while ($obj = $result->fetch_object()) {
            $users[$obj->userid] = $this->getUserById($obj->userid);
        }
        $result->close();
        return $users;
    }

    /**
     * Function to get the total number of users.
     *
     * @access public
     * @return int    The number of users.
     */
    public function getNumberOfUsers()
    {
        $sql = 'SELECT COUNT(`userid`) AS `count` FROM `users`;';
        $obj = $this->_getObject($sql);
        return $obj->count;
    }

    /**
     * Function to change the role of a user.
     *
     * @param int $userId The userId of the user to update.
     * @param int $role   The role to give the user.
     *
     * @access public
     * @return bool True on success
     */
    public function changeUserRole($userId, $role)
    {
        $this->_escape($userId);
        $this->_escape($role);
        
        $sql = 'UPDATE `users` SET `roleid`='.$role.' WHERE `userid`='.$userId
            . ' LIMIT 1;';
        $this->_execute($sql);
        
        return true;
    }

    /**
     * Function to delete a category.
     *
     * @param int $categoryId The categoryId of the category to delete.
     *
     * @access public
     * @return bool True on success
     */
    public function deleteCategory($categoryId)
    {
        $this->_escape($categoryId);

        $sql = 'DELETE FROM `categories` WHERE `categoryid`='.$categoryId.' LIMIT 1';
        $this->_execute($sql);

        return true;
    }

    /**
     * Function to update a category.
     *
     * @param object $category The category with updated info.
     *
     * @access public
     * @return bool True on success
     */
    public function updateCategory($category)
    {
        $this->_escape($category);

        $sql = 'UPDATE `categories` SET `parent`='.$category->parent
            . ', `name`=\''.$category->name.'\',  `description`=\''
            . $category->description.'\' WHERE `categoryid`='.$category->id
            . ' LIMIT 1';
        $this->_execute($sql);

        return true;
    }

    /**
     * Function to add a category.
     *
     * @param object $category The new Category.
     *
     * @access public
     * @return bool True on success
     */
    public function addCategory($category)
    {
        $this->_escape($category);

        $sql = 'INSERT INTO `categories` (`name` , `description` , `parent`) '
            . 'VALUES (\''.$category->name.'\', \''.$category->description
            . '\', '.$category->parent.');';
        $this->_execute($sql);

        return true;
    }

    /**
     * Function to get all the stores based on the userid
     *
     * @param int $userId userId of user to get stores for.
     *
     * @access public
     * @return array  An array of Store objects belonging to the user.
     */
    public function getStoresforOwner($userId)
    {
        $this->_escape($userId);

        $stores = array();
        $sql    = 'SELECT `storeid` '
            . 'FROM `storeowners` '
            . 'WHERE `userid`=' . $userId . ';';
        
        $result = $this->_getResult($sql);

        $stores = array();
        while ($obj = $result->fetch_object()) {
            $stores[] = $this->getStoreById($obj->storeid);
        }
        
        $result->close();
        return $stores;
    }

    /**
     * Function to get all the events based on the userid
     *
     * @param int $userId userId of user to get events for.
     *
     * @access public
     * @return array  An array of Event objects belonging to the user.
     */
    public function getEventsforOwner($userId)
    {
        $this->_escape($userId);

        $stores = array();
        $sql    = 'SELECT `eventid` '
            . 'FROM `eventowners` '
            . 'WHERE `userid`=' . $userId . ';';
        
        $result = $this->_getResult($sql);

        $events = array();
        while ($obj = $result->fetch_object()) {
            $events[] = $this->getEventById($obj->eventid);
        }
        
        $result->close();
        return $events;
    }
}

?>